﻿
//Dom, operates the document object.
LightScript.using("LightScript.Core", "LightScript.Array", function()
{
    var thisClass = LightScript.Core.createClass("LightScript.Dom");

    //# Private Members
    resolveXPath.selectByIdPattern = /\/\/\*\[@id=['"](.*)['"]\]/i;
    resolveXPath.predicatePattern = /\[(.*)\]/;
    resolveXPath.expressionPattern = /\W|\w+/g;
    resolveXPath.mathPattern = /[\+ \- \* \( \)]|div|mod|\d+/g;
    resolveXPath.spacePattern = /^\s+|\s+$/g;

    //# Constructions

    //# Public Members

    //# Private Methods

    /*
        e.g: 
        //*[@id='myNode']/.././div/input[@name='text']/* | /div[span>10 and or span<=12]/img[1, first(),  last()-8]
        /html/head
        /html/body/div/span/img
        /html/body/div//span
    */
    function resolveXPath(xpath)
    {
        var ret;
        if(xpath.indexOf("/") < 0)  //Only one step
        {
            ret = document.getElementsByTagName(xpath);
            if(ret && ret[0])
                ret = ret[0].childNodes;
        }
        else
        {
            ret = [];
            var dblSlash = "\\\\";
            var paths = xpath.split("|"); //Multiple paths
            var steps, step, lastNodes;
            var slen = paths.length;
            var temp, temps;
            for (var si = 0; si < slen; si++)   //Resolve each step
            {
                lastNodes = null;
                steps = paths[si].replace("//", dblSlash).split("/");
                for (var i = 0, len = steps.length; i < len; i++) //Resolve each step
                {
                    step = steps[i].replace(dblSlash, "//").replace(/^\s+|\s+$/g,"");
                    if(lastNodes)  //resolvePath always returns array-like object
                    {
                        temps = [];
                        for (var j = 0, len2 = lastNodes.length; j < len2; j++)
                        {
                            temp = resolvePath(lastNodes[j], step);
                            if(temp)
                                LightScript.Array.pushAny(temps, temp);
                        }
                        lastNodes = temps;
                    }
                    else
                    {
                        lastNodes = resolvePath(null, step);  //The beginning node
                        if(lastNodes.length == 0) break;   //The beginning  node of this path doesn't exist, so skip all the steps of this path
                    }

                    if(!lastNodes) break;
                }
                if(!lastNodes) continue;
                LightScript.Array.pushAny(ret, lastNodes);
            }
        }

        return ret;
    }

    // Always return array-like object or null.
    //e.g: html    div//span
    function resolvePath(lastNodes, step)
    {
        LightScript.trace([lastNodes, step]);
        var ret;
        var selectByIdPattern = resolveXPath.selectByIdPattern ;  //*[@id='myNode']
        var temp, temps;

        if(!lastNodes)
        {
            var temps = step.match(selectByIdPattern); //SelectById, especially optimized.
            if(temps && temps[1])
            {
                lastNodes = get(temps[1]);
                step = step.replace(selectByIdPattern, "");
            }
        }

        var predicatePattern = resolveXPath.predicatePattern ;
        var predicateExp = step.match(predicatePattern);  //[span>10 and or span<=12]
        if(predicateExp)
        {
            predicateExp = predicateExp[1];
            step = step.replace(predicatePattern, "");
        }

        var nodeTest;   //Like in /html/body/div//span, after the double slash
        var dblSlashIndex = step.indexOf("//");
        if(dblSlashIndex >= 0)
        {
            nodeTest = step.substring(dblSlashIndex + 2); //2 is the length of double slash
            step = step.substring(0, dblSlashIndex);
        }

        var axis = step;
        LightScript.trace("axis: " + axis);
        switch (axis.toLowerCase())
        {
            case ".":  //Current
                ret = lastNodes;
                break;
            case "..": //Parent
                ret = lastNodes.parentNode;
                break;
            case "":   //Root
            case "html":
                ret = lastNodes || document.documentElement;
                break;
            case "*": //All children and grandchildren
                ret = (lastNodes || document).getElementsByTagName(axis);
                break;
            default :  //Children
                ret = getChildNodesByTagName(lastNodes || document, axis);
                break;
        }

        LightScript.trace("nodeTest: " + nodeTest);
        //LightScript.trace("predicateExp: " + predicateExp);

        if(ret)
        {
            if(!(ret.length >= 0)) // Encapsulate to array
                ret = [ret];

            if(nodeTest && ret.length > 0)
            {
                temps = [];;
                for (var i = 0, len = ret.length; i < len; i++)
                {
                    temp = ret[i].getElementsByTagName(nodeTest);
                    LightScript.Array.pushAny(temps, temp);
                }
                ret = temps;
            }
            if(predicateExp)
                ret = resolvePredicate(ret, predicateExp);
        }

        LightScript.trace("ret:  " + ret);
        return ret || null;
    }

    //e.g: //*[@id='myNode']/.././div/input[@name='text']/* | /div[span>10 and or span<=12]/img[1, first(),  last()-8]
    function resolvePredicate(nodes, expression)
    {
        var ret = [];
        var isAnd = expression.indexOf(" and ") >= 0;
        var isOr = expression.indexOf(" or ") >= 0;
        var predicates, found;
        var temp, total, e;

        if(isAnd || isOr)
        {
            ret = [];
            total = [];
            if(isAnd)
                predicates = expression.split(" and ");
            else
                predicates = expression.split(" or ");
            for (var i = 0, len = predicates.length; i < len; i++) 
            {
                temp = resolveSinglePredicate(nodes, predicates[i]);
                for (var j = 0, len2 = temp.length; j < len2; j++)
                {
                    e = temp[j];
                    //Check whether already exists in total
                    found = false;
                    for (var k = 0, len3 = total.length; k < len3; k++)
                    {
                        if(total[k] == e)
                        {
                            found = true;
                            break;
                        }
                    }
                    if(!found)   //Add to the total
                        total.push(e);
                    else if(isAnd)
                     {
                        //Check whether already exists in ret
                         found = false;
                         for (var k = 0, len3 = ret.length; k < len3; k++)
                        {
                            if(ret[k] == e)
                            {
                                found = true;
                                break;
                            }
                        }
                        if(!found)   //If isAnd and hasn't been in ret
                            ret.push(e);
                     }
                }
            }

            if(isOr)
                ret = total;
        }
        else
        {
            temp = resolveSinglePredicate(nodes, expression);
            if(temp)
                LightScript.Array.pushAny(ret, temp);
        }

        return ret;
    }

    //A operator B
    //e.g: @name='myNode'    span>10
    function resolveSinglePredicate(nodes, expression)
    {
        expression = expression.replace(resolveXPath.spacePattern, "").replace("()", "__");   //last(), position()
        var ret = [];
        var operandA = null, operandB = null, operator = null;
        var isAttr = expression.indexOf("@") == 0;  //Attribute
        var isOperandBString = expression.search(/'"/) >= 0;   //Compare as String
        var exps = (isAttr ? expression.substring(1) : expression).match(resolveXPath.expressionPattern);
        var exp;

        for (var i = 0, len = exps.length; i < len; i++)
        {
            exp = exps[i];
            if(exp && exp.search(/^\s|'|"$/) < 0) //Not space, ' or "
            {            
                if(operandA === null)
                    operandA = exp;
                else if(operator === null)
                    operator = exp;
                else if(exp == "=") //Like >=, <=
                    operator += exp;
                else if(operandB === null)
                    operandB = exp;
                else
                    operandB += exp;
            }
        }

        var isMathOperator = operator ? (operator.search(/[><=]/) < 0) : false;
        var isExistenceCheck = false; //Only check  whether exists, like [@name], [span]
        var temp, temps;
        var literalIndex;

        if(!operator)  //Like [1], [last()], [@name], [span]
        {
            if(operandA == "last__")
                literalIndex = nodes.length;
            else
                literalIndex = parseInt(operandA);
            if(!isNaN(literalIndex))
                ret = nodes[literalIndex - 1];
            else
                isExistenceCheck = true;
        }

        if(isMathOperator) //Like [last()-1]
        {
            if(operandA == "last__")
                temp = nodes.length - 1;
            else
                temp = operandA;
            ret = nodes[resolveMathOperation(temp + operator + operandB)];
        }
        else if(isNaN(literalIndex))
        {
            for (i = 0, len = nodes.length; i < len; i++) 
            {
                e = nodes[i];

                if(isExistenceCheck)   //Like [@name], [span]
                {
                    if(isAttr)
                    {
                        if(e.getAttribute(operandA) != null)
                            ret.push(e);
                    }
                    else
                    {
                        temps = e.getElementsByTagName(operandA);
                        if(temps && temps[0])
                            ret.push(e);
                    }
                }
                else
                {
                    if(operandA == "position__") //[position()<3]
                    {
                        if(resolveLogisticOperation(i+1, operator, resolveMathOperation(operandB)))
                            ret.push(e);
                    }
                    else if(isAttr) //[@name='myNode']
                    {
                        if(resolveLogisticOperation(e.getAttribute(operandA), operator, resolveMathOperation(operandB)))
                            ret.push(e);
                    }
                    else //[span>10]
                    {
                        temps = e.getElementsByTagName(operandA);
                        if(temps && temps[0] )
                        {
                            temp = temps[0].innerHTML;
                            if(!isOperandBString)    //Convert to number
                                temp *= 1;
                            if(resolveLogisticOperation(temp, operator, resolveMathOperation(operandB)))
                                ret.push(e);
                        }
                    }
                }
            }
        }

        return ret;
    }

    function resolveLogisticOperation(operandA, operator, operandB)
    {
        var ret = false;
        switch (operator)
        {
            case ">":
                ret = operandA > operandB;
                break;
            case "<":
                ret = operandA < operandB;
                break;
            case "=":
                ret = operandA == operandB;
                break;
            case "<=":
                ret = operandA <= operandB;
                break;
            case ">=":
                ret = operandA >= operandB;
                break;
            case "!=":
                ret = operandA != operandB;
                break;
            default :
                break;
        }

        return ret;
    }

    function resolveMathOperation(expression)
    {
        // + - * div mod  1+2*((3+7) div 2))*9-5   1+(2+(3+4 div 5*6)+7)    1+2*3
        var chars = expression.replace(/\s/g, "").match(resolveXPath.mathPattern);
        if(!chars)
            return expression;
        var num, ch;
        var stack = [];
        var opA, opB, opr;

        for (var i = 0, len = chars.length; i < len; i++)
        {
            ch = chars[i];
            if(stack.length < 2)
                stack.push(ch);
            else
            {
                if(ch == "(")
                    stack.push(ch);
                else if(ch == ")")
                {
                    if(stack[stack.length -2] != "(")
                    {
                        opB = stack.pop();
                        opr = stack.pop();
                        opA = stack.pop();
                        num  = doMathOperation(opA, opr, opB);
                        stack.pop();    //Pop "("
                        stack.push(num);
                    }
                    else
                    {
                        num = stack.pop();
                        stack.pop();    //Pop "("
                        stack.push(num);
                    }
                    while (stack.length >=2 && stack[stack.length-2].search(/[^\(\d]/)>=0)
                    {
                        stack.push(doMathOperation(stack.pop(), stack.pop(), stack.pop()));
                    }
                }
                else if(chars[i-1] !="(" && ch.search(/\d/)==0 && 
                    (i+1==len || !(chars[i+1].search(/\*|div|mod/) >= 0 && (chars[i-1].search(/\+|\-/) >= 0))))
                {
                    opr = stack.pop();
                    opB = stack.pop();
                    stack.push(doMathOperation(opB, opr, ch));
                }
                else
                   stack.push(ch);
            }
        }

        return stack.length > 1 ? doMathOperation(stack.pop(), stack.pop(), stack.pop()) : stack[0];
    }

    function doMathOperation(operandA, operator, operandB)
    {
        var ret;
        operandA = operandA * 1;
        operandB = operandB * 1;
        switch (operator)
        {
            case "+":
                ret = operandA + operandB;
                break;
            case "-":
                ret = operandA - operandB;
                break;
            case "*":
                ret = operandA * operandB;
                break;
            case "div":
                ret = operandA / operandB;
                break;
            case "mod":
                ret = operandA % operandB;
                break;
            default :
                break;
        }

        return ret;
    }

    //# Public Methods
    thisClass.get = get;
    function get(id)
    {
        var e = id;
        if(typeof(e) == "string")
            e = document.getElementById(e);

        return e;
    }

    thisClass.selectNodes = selectNodes;
    function selectNodes(xpath)
    {
        return resolveXPath(xpath);
    }

    thisClass.selectSingleNode = selectSingleNode;
    function selectSingleNode(xpath)
    {
        var ret = resolveXPath(xpath);
        return ret ? ret[0] : null;
    }

    thisClass.getElementsByClassName = getElementsByClassName;
    function getElementsByClassName(className, tagName, e)
    {
        var ret = [];
        var child, classNames;
        var children = (e || document).getElementsByTagName(tagName || "*");
        for (var i = 0, len = children.length; i < len; i++)
        {
            child = children[i];
            classNames = child.className.split(" ");
            for (var j = 0, len2 = classNames.length; j < len2; j++)
            {
                if (classNames[j] == className)
                {
                    ret.push(child);
                    break;
                }
            }
        }

        return ret;
    }

    thisClass.getChildNodesByTagName = getChildNodesByTagName;
    function getChildNodesByTagName(e, tagName)
    {
        var ret;
        if (e && e.childNodes)
        {
            ret = [];
            tagName = tagName.toUpperCase();
            for (var i = 0, len = e.childNodes.length; i < len; i++)
            {
                if (e.childNodes[i].tagName == tagName)
                    ret.push(e.childNodes[i]);
            }
        }
        return ret;
    }

    thisClass.getElementsByLocation = getElementsByLocation;
    function getElementsByLocation(clientX, clientY, tagName, e)
    {
        var ret = [];
        var es = (e || document.body).getElementsByTagName(tagName || "*");
        var e, rect;
        for (var i = 0, len = es.length; i < len; i++)
        {
            e = es[i];
            rect = e.getBoundingClientRect();
            if (rect.left <= clientX && clientX <= rect.left + e.offsetWidth && rect.top <= clientY && clientY <= rect.top + e.offsetHeight)
                ret.push(e);
        }

        return ret;
    }

    thisClass.getScrollBounds = getScrollBounds;
    function getScrollBounds()
    {
        var left = window.pageXOffset || document.documentElement.scrollLeft || document.body.scrollLeft;
        var top = window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop;

        return { x: left, y: top };
    }

    thisClass.getClientBounds = getClientBounds;
    function getClientBounds()
    {
        var width = window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth;
        var height = window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight;

        return { width: width, height: height };
    }

    // Gets the absolute position of an element. 
    thisClass.getPosition = getPosition;
    function getPosition(e)
    {
        var l = e.offsetLeft;
        var t = e.offsetTop;
        while(e = e.offsetParent)
        {
                l += e.offsetLeft;
                t += e.offsetTop;
        }

        return { x: l, y: t };
    }

    thisClass.setAbsoluteLocation = setAbsoluteLocation;
    function setAbsoluteLocation(e, x, y)
    {
        if (isNaN(x)) x *= 1;
        if (isNaN(y)) y *= 1;

        e.style.left = x + "px";
        e.style.top = y + "px";

        var p = e;
        var x1 = e.offsetLeft;
        var y1 = e.offsetTop;
        while (p = p.offsetParent)
        {
            x1 += p.offsetLeft;
            y1 += p.offsetTop;
        }

        if (x != x1) e.style.left = (2 * x - x1) + "px";
        if (y != y1) e.style.top = (2 * y - y1) + "px";
    }

    thisClass.hasOverlap = hasOverlap;
    function hasOverlap(e1, e2, multiplicity)
    {
        var rect1 = e1.getBoundingClientRect();
        var rect2 = e2.getBoundingClientRect();
        var rect3 = {};

        rect3.left = Math.max(rect1.left, rect2.left);
        rect3.top = Math.max(rect1.top, rect2.top);
        rect3.right = Math.min(rect1.right, rect2.right);
        rect3.bottom = Math.min(rect1.bottom, rect2.bottom);

        if (rect3.right >= rect3.left && rect3.bottom >= rect3.top)
        {
            if (!multiplicity)
                return true;
            else
            {
                var area1 = (rect1.right - rect1.left) * (rect1.bottom - rect1.top);
                var area3 = (rect3.right - rect3.left) * (rect3.bottom - rect3.top);
                return area3 >= area1 * multiplicity;
            }
        }
        else
            return false;
    }

    thisClass.setAttributes = setAttributes;
    function setAttributes(e, attributes)
    {
        var item;
        for (var p in attributes)
        {
            item = attributes[p];
            if (item == null || typeof (item) == "undefined") continue;
            if (p.toLowerCase() == "style")
            {
                for (var s in item)
                    e[p][s] = item[s];
            }
            else
                e[p] = item;
        }
    }

    // Creates an element and add it to the specific container.
    //tag<String>: The tagName.
    //attributes<Object>: The attributes. e.g: {id: "ea1", {style: {cssText: ""}}}.
    //parentNode<String/Object>: Container node(optional ,default is document.body).
    //return<Object>: The added element.
    thisClass.addNode = addNode;
    function addNode(tag, attributes, parentNode)
    {
        parentNode = parentNode ? get(parentNode) : document.body;
        var e = document.createElement(tag);
        setAttributes(e, attributes);
        parentNode.appendChild(e);

        return e;
    }

    thisClass.createPopup = createPopup;
    function createPopup(popupAttributes, allowTransparency, parentNode)
    {
        var popupContainer = document.createElement("div");

        var popup = document.createElement("div");
        setAttributes(popup, popupAttributes);

        var popupFrame = document.createElement("iframe");
        if (allowTransparency)
        {
            popupFrame.allowTransparency = true;
            popupFrame.style.cssText = "opacity: 0.0; filter: alpha(opacity=0);";
        }
        popupFrame.style.border = "0px";

        if (popup.style.zIndex)
        {
            popupContainer.style.zIndex = popup.style.zIndex;
            popupFrame.style.zIndex = popup.style.zIndex - 1;
        }
        else
        {
            popupContainer.style.zIndex = 10000;
            popupFrame.style.zIndex = 99999;
        }

        if (!parentNode) parentNode = document.body;

        popupContainer.appendChild(popup);
        popupContainer.appendChild(popupFrame);
        parentNode.appendChild(popupContainer);

        popupFrame.style.width = popup.offsetWidth + "px";
        popupFrame.style.height = popup.offsetHeight + "px";

        popupContainer.body = popup;
        var _setSize = function(w, h)
        {
            popupContainer.style.width = popup.style.width = popupFrame.style.width = parseInt(w) + "px";
            popupContainer.style.height = popup.style.height = popupFrame.style.height = parseInt(h) + "px";
        }
        var _resetSize = function()
        {
            popupContainer.style.width = popupFrame.style.width = popup.offsetWidth + "px";
            popupContainer.style.height = popupFrame.style.height = popup.offsetHeight + "px";
        }

        popupContainer.setSize = _setSize;
        popupContainer.resetSize = _resetSize;

        popupContainer.style.position = "absolute";

        try
        {
           return popupContainer; 
        }
        catch (ex) {}
        finally
        {
            popupContainer = null;
            popup = null;
            popupFrame = null;
            _setSize = null;
            _resetSize = null;
        }        
    }

    thisClass.fireEvent = fireEvent;
    function fireEvent(e, eventName)
    {
        if (e.fireEvent)
            e.fireEvent("on" + eventName);
        else
        {
            var ev = document.createEvent("Events");
            ev.initEvent(eventName, true, true);
            e.dispatchEvent(ev);
        }
    }

    thisClass.stopEvent = stopEvent;
    function stopEvent(ev)
    {
        if(!ev) ev = event;

        if (ev.preventDefault)
        {
            ev.preventDefault();
            ev.stopPropagation();
        }
        else
        {
            ev.cancelBubble = true;
            ev.returnValue = false;
        }
    }

    // Adds style from an array.
    //style<Array>: e.g: [[".a", "display: none;"],["#b", "width: 1px;"],["body", "margin: 0px;"]]
    thisClass.addStyle = addStyle;
    function addStyle(style)
    {
        if (!document.styleSheets || !document.createStyleSheet)
        {
            //opera & firefox
            var swap = [];
            var sheet = document.createElement("style");
            for (var i = 0, len = style.length; i < len; i++)
                swap.push(style[i][0] + " { " + style[i][1] + " } ");
            sheet.innerHTML = swap.join("\n");
            document.body.appendChild(sheet);
        }
        else
        {
            //ie
            if (!document.styleSheets.length)
                document.createStyleSheet();
            var sheet = document.styleSheets[document.styleSheets.length - 1];
            for (var i = 0, len = style.length; i < len; i++)
                sheet.addRule(style[i][0], style[i][1]);
        }
    }

    // Loads the stylesheets from css file.
    //url<String>: The url of the css file.
    //parentNode<String/Object>: The parent node which the the css file will load to (optional, default is document.body)
    thisClass.loadStyle = loadStyle;
    function loadStyle(url, attributes, parentNode)
    {
        if (!attributes)
            attributes = {};
        attributes.type = "text/css";
        attributes.rel = "stylesheet";
        attributes.href = url;
        addNode("link", attributes, parentNode);
    }

    //Inserts text, javascript and css e.t.c.
    //e<HtmlElement>: The container.
    ///htmlCode<String>: The content.
    thisClass.setInnerHTML = setInnerHTML;
    function setInnerHTML(e, htmlCode)
    {
        var ua = navigator.userAgent.toLowerCase();
        if (ua.indexOf("msie") >= 0 && ua.indexOf("opera") < 0)
        {
            htmlCode = "<div style='display:none'>for IE</div>" + htmlCode;
            htmlCode = htmlCode.replace(/<script([^>]*)>/gi, "<script$1 defer>");
            e.innerHTML = htmlCode;
            e.removeChild(e.firstChild);
        }
        else
        {
            var el_next = e.nextSibling;
            var el_parent = e.parentNode;
            el_parent.removeChild(e);
            e.innerHTML = htmlCode;
            if (el_next)
                el_parent.insertBefore(e, el_next)
            else
                el_parent.appendChild(e);
        }
    }    

    thisClass.resizeImage = resizeImage;
    function resizeImage(e, w, h)
    {
        if (!w) w = e.width;
        if (!h) h = e.height;
        var img = new Image();
        img.src = e.src;
        if (img.width > 0 && img.height > 0)
        {
            if (img.width / img.height >= w / h)
            {
                if (img.width > w)
                {
                    e.width = w;
                    e.height = img.height * w / img.width;
                }
                else
                {
                    e.width = img.width;
                    e.height = img.height;
                }
            }
            else
            {
                if (img.height > h)
                {
                    e.height = h;
                    e.width = (img.width * h) / img.height;
                }
                else
                {
                    e.width = img.width;
                    e.height = img.height;
                }
            }
        }
    }

    thisClass.setDrag = setDrag;
    function setDrag(o)
    {
        o.onmousedown = function(a)
        {
            var d = document;
            if (!a) a = window.event;
            var x = a.layerX ? a.layerX : a.offsetX, y = a.layerY ? a.layerY : a.offsetY;
            if (o.setCapture)
                o.setCapture();
            else if (window.captureEvents)
                window.captureEvents(Event.MOUSEMOVE | Event.MOUSEUP);

            d.onmousemove = function(a)
            {
                if (!a) a = window.event;
                if (!a.pageX) a.pageX = a.clientX;
                if (!a.pageY) a.pageY = a.clientY;
                var tx = a.pageX - x, ty = a.pageY - y;
                o.style.left = tx;
                o.style.top = ty;
            };

            d.onmouseup = function()
            {
                if (o.releaseCapture)
                    o.releaseCapture();
                else if (window.releaseEvents)
                    window.releaseEvents(Event.MOUSEMOVE | Event.MOUSEUP);
                d.onmousemove = null;
                d.onmouseup = null;
            };
        };
        if (o.style.position != "absolute") o.style.position = "absolute";
    }

    //# Shortcuts
    LightScript.addShortcut("get", thisClass.get);
});
